/* 
 * EK
 * 
 * Enrichment Kit 0.4.3 (http://enrichmentkit.com)
 * Copyright (c) 2007 Pink Hominid
 * Licensed under terms of the MIT License (LICENSE.txt)
 * 
 * IE 6.0+, FF 1.5+ Tested
 * 
 * Convention >> http://javascript.crockford.com/code.html
 */

if ( typeof window.EK == "undefined" ) { 

    /**
     * EK namespace & general utils
     */
    var EK = {	
        /**
         * Browser type and version
         * Thanks jQuery :)
         */
        browser: {
            version: (navigator.userAgent.match(
                /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/i) || [])[1],
            msie: /msie/i.test(navigator.userAgent) &&
                !(/opera/i.test(navigator.userAgent)),
            mozilla: /mozilla/i.test(navigator.userAgent) &&
                !(/(compatible|webkit)/i.test(navigator.userAgent))
        },
        
        /**
         * Writes an object to the Firebug debug console, if it exists. 
         * @param obj {Object} The object to write to the console
         */
        debug: function (obj) {
            if (window.console && console.debug) { console.debug(obj); }
        },

        /**
         * Writes an object to the Firebug error console, if it exists. 
         * @param obj {Object} The object to write to the console
         */
        error: function (obj) {
            if (window.console && console.error) { console.error(obj); }
        },
		
        /**
         * Thanks Prototype :)
         * @param html {Object}
         */
        escapeHtml: function (html) {
            var div = document.createElement('div');
            var text = document.createTextNode(html);
            div.appendChild(text);
            return div.innerHTML;
        },

        /**
         * Simple inheritance. Thanks jQuery :)
         * @param target {Object} The Object to extend
         * @param obj1 {Object} An Object to extend with
         * @param objN {Object} More Objects to extend with
         */
		extend: function (/*target, obj1, [objN]*/) {
			var target = arguments[0] || {}, a = 1, al = arguments.length, prop;
		
			for ( ; a < al; a++ )
				// Only deal with non-null/undefined values
				if ( (prop = arguments[a]) != null )
					// Extend the base object
					for ( var i in prop ) {
						// Prevent never-ending loop
						if ( target == prop[i] )
							continue;
		
						// Don't bring in undefined values
						if ( prop[i] != undefined )
							target[i] = prop[i];
					}
		
			// Return the modified object
			return target;
		},
        
        /**
         * Implements Mozilla's Function name property for IE
         * @param {Object} fn
         */
        functionName: function (fn) {
            if (this.browser.mozilla) {
                return fn.name;				
            }
            else {
                var matches = /^function\s*([^(\s]*)/.exec(fn.toString());
                if (matches && matches.length > 0) { return matches[1]; }
            }
        },
        
        /**
         * Shorthand for getElementById
         * "Pro JavaScript Techniques" (Resig, 2006 Apress)
         * @param name {String} The ID of the element to get
         * @return {HTMLElement} The element object
         */
        id: function (name) {
            return document.getElementById(name);
        },
        
        /**
         * Prepends content to the inside of the given element
         * @param content {String} Content to prepend
         * @param elem {HTMLElement} The element to prepend html into
         */
        prepend: function (content, elem) {
            elem.innerHTML = content + elem.innerHTML;
        },
        
        /**
         * Purges references in the given DOM element that cause memory leaks in
         * IE. Should be called before removing any element, either by the
         * removeChild method, or by setting the innerHTML property.
         * See http://javascript.crockford.com/memory/leak.html
         * @param domElem {HTMLElement}
         */
        purge: function (domElem) {
            var a = domElem.attributes, i, l, n;
            if (a) {
                l = a.length;
                for (i = 0; i < l; i += 1) {
                    n = a[i].name;
                    if (typeof domElem[n] === 'function') {
                        domElem[n] = null;
                    }
                }
            }
            a = domElem.childNodes;
            if (a) {
                l = a.length;
                for (i = 0; i < l; i += 1) {
                    this.purge(domElem.childNodes[i]);
                }
            }
        },        

        /**
         * Shorthand for getElementsByTagName
         * "Pro JavaScript Techniques" (Resig, 2006 Apress)
         * @param name {String} The ID of the element to get
         * @param [elem] {HTMLElement} Optional element to scope search
         * @return {Array} The matching elements
         */
        tag: function (name, elem) {
            return (elem || document).getElementsByTagName(name);
        },

        /**
         * Thanks Prototype :)
         * @param html {Object}
         */
        unescapeHtml: function (html) {
            var div = document.createElement('div');
            div.innerHTML = html.replace(/<\/?[^>]+>/gi, '').replace(/&apos;/g,"'");
            return div.childNodes[0].nodeValue;
        },

        /**
         * Version String
         */
        version: "EK 0.4.3",
		
        /**
         * Writes an object to the Firebug warn console, if it exists. 
         * @param obj {Object} The object to write to the console
         */
        warn: function (obj) {
            if (window.console && console.warn) { console.warn(obj); }
        }
    };
} else {
    throw("What th'EK?! 'EK' namespace conflict.");
}
/*
 * EK.location
 * Inspired by jQuery history plugin (Taku Sano)
 * 
 * Enrichment Kit 0.4.3 (http://enrichmentkit.com)
 * Copyright (c) 2007 Pink Hominid
 * Licensed under terms of the MIT License (LICENSE.txt)
 * 
 * IE 6.0+, FF 1.5+ Tested
 * 
 * Known issues:
 * 
 * IE 6.0  >> After a manual hash change, history gets whacked and the location
 *            bar address and window title stick
 *         >> Incompatible with Firebug Lite (1.0-b1) due to use of iframe
 *         
 * FF 1.5+ >> After a manual hash change, the location bar address sticks
 */

/**
 * Singleton providing enriched location & history support,
 * including named history entries
 */
EK.location = new function EkLocation() {
    this.debugMode = false;

    this.change = function (callback) {
        if (arguments.length) {
            var isFirstListener = !changeListeners.length;
            changeListeners.push(callback);
            if (isFirstListener) { init(); }
        } else {
            for (var i = 0; i < changeListeners.length; i++) {
                changeListeners[i](location.href);
            }
        }
    };

    this.unbindChange = function (callback) {
        if (arguments.length) {
            var startLength = changeListeners.length;
            for (var i = startLength - 1; i >= 0; i--) {
                if (changeListeners[i] === callback) {
                    changeListeners.splice(i, 1);
                }
            }
        } else { // unbind all
            changeListeners = [];
        }		
	};

    this.getPrevious = function () {
		return lastHref;
	};

    this.replace = function (url) {
        var iframe = getIframe();
        if(iframe) {
            iframe.title = document.title;
            iframe.location.replace(url);
        }
        location.replace(url);
        lastHref = location.href;
    };

    var that = this;
    var changeListeners = [];
    var lastHref = undefined;
    var iframe = undefined;
    
    function init() {
        debug("before init()");

        /* Bootstrap hash if it's not already present.
         * Mozilla's location.hash returns "" if "#" is not followed up with
         * other chars, hence the use of location.href & RegExp
         */
        if (!(/#/.test(location.href))) {
            location.replace(location.href + "#");
        }
        
        var currentHref = location.href;
    
        if(EK.browser.msie) {
            // inject and initialize iframe for IE history
            EK.prepend('<iframe id="ekHistory" src="javascript:\'\'" ' + 
                'style="display: none;"></iframe>', EK.tag("body")[0]);
            var iframe = getIframe();
            iframe.open();
            iframe.close();
            iframe.location.href = currentHref;
        }
    
        EK.location.change();
		
        // pickup any changes to the window title and inject into history
        EK.location.replace(location.href);
    
        debug("after init()");
        
        setTimeout(check, 100);
    }

    function check() {
        var currentHref = location.href;
        var iframe, iframeHref = undefined;
        if(EK.browser.msie) {
            iframe = getIframe();
            if (iframe) { iframeHref = iframe.location.href; }
        }
                    
        if (/^http/.test(currentHref) && (iframeHref ? /^http/.test(iframeHref):true)) {
            if(currentHref != lastHref) {
                debug("before check()");
                
                if(EK.browser.msie) {
                    iframe.open();
                    iframe.close();
                    iframe.location.href = currentHref;
                }
                            
                EK.location.change();
    
                // pickup any changes to the window title and inject into history
                EK.location.replace(location.href);
                
                debug("after check()");
                
            } else if (iframeHref && iframeHref != lastHref) {
                debug("before check()");
    
                location.href = iframeHref;
    
                EK.location.change();
    			
                // pickup any changes to the window title and inject into history
                EK.location.replace(location.href);
                
                debug("after check()");
            }
        }
        setTimeout(check, 100);
    }
    
    function getIframe() {
        var elem = EK.id("ekHistory"); 
        if (elem) { return elem.contentWindow.document; }
        else { return null; }
    }
    
    function debug(msg) {
        if (that.debugMode) {
            EK.debug("EK.location: " + msg);
            EK.debug("EK.location: lastHref = " + lastHref);
            EK.debug("EK.location: currentHref = " + location.href);
            var iframe = getIframe();
            if(iframe) {
                EK.debug("EK.location: iframeHref = " + iframe.location.href);
            }
        }
    }
}();
/*
 * EK.Rewriter
 * Inspired by Apache mod_rewrite
 * 
 * Enrichment Kit 0.4.3 (http://enrichmentkit.com)
 * Copyright (c) 2007 Pink Hominid
 * Licensed under terms of the MIT License (LICENSE.txt)
 * 
 * IE 6.0+, FF 1.5+ Tested
 */

/**
 * Manipulates URLs using regex rules.
 * @constructor
 */
EK.Rewriter = function EkRewriter() {
    
    /**
     * Set of named rewrite rules. Rules evaluated in defined order.
     */
    this.rules = { // heheh, this rules!
        
        /**
         * Trims extra space and slashes, standardizes hash usage
         * "http://blah.com#  //  an/ajax/url  //  " ->
         * "http://blah.com#/an/ajax/url"
         */
        normalize: {
            pattern: /^([^#]*)#?(?:\s|%20)*\/*(?:\s|%20)*(.*?)(?:\s|%20)*\/*(?:\s|%20)*$/,
            rewrite: "$1#/$2"
        }
    };
    
    /**
     * Processes the given URL through all the rewrite rules.
     * @param url {String} The URL to rewrite
     * @return {String} The rewritten URL
     */
    this.x = function (url) {
        var reUrl = url;
        if (typeof reUrl == "string" && this.rules) {
            for (var ruleName in this.rules) {
                var rule = this.rules[ruleName];
                if (rule.pattern && rule.pattern.test(reUrl)) {
                    if (rule.rewrite) {
                        reUrl = reUrl.replace(rule.pattern, rule.rewrite);
                    }					
                } 
            }
        }
        return reUrl;
    };
};
/*
 * EK.Dispatcher
 * Inspired by Rails Routes
 * 
 * Enrichment Kit 0.4.3 (http://enrichmentkit.com)
 * Copyright (c) 2007 Pink Hominid
 * Licensed under terms of the MIT License (LICENSE.txt)
 * 
 * IE 6.0+, FF 1.5+ Tested
 * 
 * TODO:
 * 
 * Remove dependency on eval
 */

/**
 * Maps URLs to client-side controllers/actions/params
 * @constructor
 */
EK.Dispatcher = function EkDispatcher() {
    var that = this;
    
    /**
     * Set of named dispatch routes. Routes visited in defined order.
     */
    this.routes = { 
        /*test: {
            pattern: /^[^#]*#\/([^\/]*)\/([^\/]*)\/([^\/]*)\/([^\/]*)/,
            controller: "EK.test.$1",
            action: "$2",
            params: "$3, $4"
        }*/
    };
    
	var tier = 1;
	this.tier = {
		get: function() {
			return tier;
		},
		set: function(newTier) {
			tier = newTier;
			that.x(); // dispatch with new tier
		}
	};
	
    this.controllerFactory = {
        get: function (cName) {
            var c = eval(cName);
            if (typeof c == "function") { c = new c(); }
            return c;
        }       
    };
    
    /**
     * Processes the given URL through all the dispatch routes.
     * @param url {String} The URL to dispatch
     */
    this.x = function (url) {
        var exec = [];
        var unload = [];
        url = url || location.href; // no param defaults to current url
        if (typeof url == "string" && this.routes) {
            for (var routeName in this.routes) {
                var route = this.routes[routeName];
                var matches = undefined;
                if (route.pattern &&
				    route.controller &&
					(route.tier || 1) <= that.tier.get() &&
				    route.pattern.test(url)) {

                    exec.push(route);
                } else {
                    unload.unshift(route);
                }
            }
            for (var i = 0; i < unload.length; i++) {
                if (unloadRoute(unload[i]) === false) { return false; }
            }
            for (var j = 0; j < exec.length; j++) {
                if (execRoute(exec[j], url) === false) { return false; }
            }

        }
    };
    
    function execRoute(route, url) {
        var ctrlrStr = route.controller;
        var actionStr = route.action;
        var paramsStr = route.params;
        var matches = route.pattern.exec(url);
        for (var i = 1; i < matches.length; i++) {
            ctrlrStr = ctrlrStr.replace("$" + i, matches[i]);
            if (actionStr) {
                actionStr = actionStr.replace("$" + i,
                     matches[i]); 
            }
            if (paramsStr) {
                paramsStr = paramsStr.replace("$" + i,
                    matches[i]); 
            }
        }
        var command = new EK.DispatchCommand(ctrlrStr, actionStr, paramsStr);
        if (!command.equals(route.lastCommand)) {
            if (!command.controllerEquals(route.lastCommand)) {
                if (unloadRoute(route) === false) { return false; }
            }
            var result = command.x(that.controllerFactory);
            route.lastCommand = command;
            return result;
        }        
    }
    
    function unloadRoute(route) {
        if (route.lastCommand) {
            var result = null;
            route.lastCommand.action = route.unload || "unload";
            route.lastCommand.params = null;
            if (route.lastCommand.x() === false) {
                return false;
            }
            route.lastCommand = null;
            return true;
        }        
    }
};

EK.DispatchCommand = function EkDispatchCommand(ctrlr, action, params) {
    this.controller = ctrlr;
    this.action = action;
    this.params = params;

    this.controllerRef = undefined;

    this.controllerEquals = function (cmd) {
        if (cmd &&
            (this.controller === cmd.controller))
        {
            return true;
        } else {
            return false;
        }
    };

    this.equals = function (cmd) {
        if (cmd &&
            (this.controller === cmd.controller) &&
            (this.action === cmd.action) &&
            (this.params === cmd.params)) 
        {
            return true; 
        } else {
            return false;
        }
    };
    
    this.x = function(cFactory) {
        //get controller
        if (!this.controllerRef && cFactory) {
            this.controllerRef = cFactory.get(this.controller);
        }
        if (this.controllerRef && this.action && this.controllerRef[this.action]) {
            return this.controllerRef[this.action].apply(this.controllerRef, 
                this.params ? this.params.split(",") : []);
        }
    };
};
/*
 * EK.Observable
 * Inspired by jQuery Events API
 * 
 * Enrichment Kit 0.4.3 (http://enrichmentkit.com)
 * Copyright (c) 2007 Pink Hominid
 * Licensed under terms of the MIT License (LICENSE.txt)
 * 
 * IE 6.0+, FF 1.5+ Tested
 */
EK.Observable = function EkObservable() {
    this.observers = {};

    this.trigger = function (type, data) {
        var observersOfType = this.observers[type];
        if (observersOfType) {
            for (var i = 0; i < observersOfType.length; i++) {
                observersOfType[i](data);
            }
        }
    };
    
    this.bind = function (type, fn) {
        var observersOfType = this.observers[type];
        if (observersOfType) {
            observersOfType.push(fn);
        } else {
            this.observers[type] = [fn];
        }
    };
    
    this.unbind = function (type, fn) {
		if (arguments.length == 0) { // unbind all
            this.observers = {};
		} else {
	        var observersOfType = this.observers[type];
	        if (observersOfType) {
	            if (arguments.length > 1) {
	                var startLength = observersOfType.length;
	                for (var i = startLength - 1; i >= 0; i--) {
	                    if (observersOfType[i] === fn) {
	                        observersOfType.splice(i, 1);
	                    }
	                }
	            } else { // unbind all of type
	                observersOfType = null;
	            }
	        }
		}		
    };
};
/*
 * EK.modelLocator
 * 
 * Enrichment Kit 0.4.3 (http://enrichmentkit.com)
 * Copyright (c) 2007 Pink Hominid
 * Licensed under terms of the MIT License (LICENSE.txt)
 * 
 * IE 6.0+, FF 1.5+ Tested
 * 
 * Known Issues:
 * 
 * IE 6.0+ >> Workaround for missing Object.watch requires HTML DOM
 * 
 * TODO:
 * 
 * Add support for change notification in nested model objects
 * Add support for array change notification
 * Track usage count and destroy unused models??
 * Remove dependency on eval
 */
EK.modelLocator = new function EkModelLocator() {
    var cache = {};
    
    this.mFactory = {
        get: function (mName) {
            var m = eval(mName);
            if (typeof m == "function") { m = new m(); }
            return m;
        }       
    };
    
    this.x = function (name, observeDepth) {
		observeDepth = observeDepth || 0;
        var model = undefined;
        if (name) {
            model = cache[name];
            if (!model) {
                model = this.mFactory.get(name);
				
                if (EK.browser.msie) {
                    var watchableModel = document.createElement("meta");
                    watchableModel.id = name.replace(/\./g,"");
					EK.extend(watchableModel, model, new EK.Observable());
                    EK.tag("head")[0].appendChild(watchableModel);
                    watchableModel.constructor = model.constructor;
                    model = watchableModel;
                    model.onpropertychange = function () {
                        model.trigger("change", event.propertyName);
                    };
                } else { // assumes Object watch supported
                    for (var propertyName in model) {
                        model.watch(propertyName,
                            function (prop, oldval, newval) {
                                setTimeout(function () {
                                    model.trigger("change", prop); }, 10);
                                return newval;
                            });
					}
                    EK.extend(model, new EK.Observable());
                }
                cache[name] = model;
            }
        }
        return model;
    };
	
	this.purgeModel = function (name) {
        var model = undefined;
        if (name) {
            model = cache[name];
            if (model) {
                if (EK.browser.msie) {
                    model.onpropertychange = null;
                    model.unbind();
                    var modelId = name.replace(/\./g,"");
					EK.purge(EK.tag("head")[0].removeChild(EK.id(modelId)));
                } else { // assumes Object unwatch supported
                    for (var propertyName in model) {
                        model.unwatch(propertyName);						
					}
                    model.unbind();
                }
                cache[name] = null;				
			}
		}
	};
}();
